#
# Package.
#

package iSOCK::Controller::REST;

#
# Parent.
#

use Moose;
use namespace::autoclean;

BEGIN { extends 'Catalyst::Controller::REST' }

#
# Modules.
#

use DateTime;
use JSON qw( decode_json );
use List::MoreUtils qw( uniq );
use SOCK::BOT qw( last_breath normalized_version_string );
use Sort::Naturally qw( ncmp nsort );
use Try::Tiny;
use YAML qw( Dump );

#
# Interface.
#

sub component_types     : Local : ActionClass('REST') {}

sub components          : Local : ActionClass('REST') {}
sub component           : Local : ActionClass('REST') {}

sub devices             : Local : ActionClass('REST') {}

sub lines               : Local : ActionClass('REST') {}

sub processes           : Local : ActionClass('REST') {}

sub stages              : Local : ActionClass('REST') {}

sub scm_types           : Local : ActionClass('REST') {}

sub products            : Local : ActionClass('REST') {}
sub product             : Local : ActionClass('REST') {}

sub repositories        : Local : ActionClass('REST') {}
sub repository          : Local : ActionClass('REST') {}
sub repository_contents : Local : ActionClass('REST') {}
sub repository_types    : Local : ActionClass('REST') {}

sub commit              : Local : ActionClass('REST') {}

sub specifications      : Local : ActionClass('REST') {}
sub specification       : Local : ActionClass('REST') {}

sub datasheets          : Local : ActionClass('REST') {}
sub datasheet           : Local : ActionClass('REST') {}

sub user                : Local : ActionClass('REST') {}
sub users               : Local : ActionClass('REST') {}

sub hits                : Local : ActionClass('REST') {}

sub job                 : Local : ActionClass('REST') {}
sub last_job            : Local : ActionClass('REST') {}

sub harvest             : Local : ActionClass('REST') {}
sub last_harvest        : Local : ActionClass('REST') {}

sub search              : Local : ActionClass('REST') {}

sub dstp                : Local : ActionClass('REST') {}

sub mail                : Local : ActionClass('REST') {}

sub default : Path {
  my ( $self, $c ) = @_;
  
  $self->status_bad_request ( $c, message => 'not yet implemented' );
}

#
# Boilerplate.
#

sub _boilerplate_POST : Private {
  my ( $self, $c, $re ) = @_;
  
  my ( $entity, $message );

  my $parameters = $c->request->parameters;

  if ( my ( $table, $id ) = ( $parameters->{id} =~ $re ) ) {
    my $Table = ucfirst $table;

    if ( my $row = $c->model ( "App::$Table" )->find ( $id ) ) {
        $row->set_column ( $parameters->{field} => $parameters->{value} );

        try {
          if ( $row->update ) {
            $entity  = { value => $parameters->{value} };
          }
          else {
            $message = 'problem during update';
          }
        catch {
          $message = "exception during update: $_";
        };
      }
    }
    else {
      $message = "unknown \"$id\" \L$table";
    }
  }
  else {
    $message = "unsupported \"$parameters->{id}\" parameter";
  }
  
  return ( $entity, $message );
}

#
# Authorizations.
#

sub auto : Private {
  my ( $self, $c ) = @_;
  
  if ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) {
    if ( $c->request->parameters->{hash} ) {
      $c->session;

      $c->stash->{user_id} = $user_r->id;

     #$self->busy ( $c );

      if ( $c->request->path =~ /^ rest \/ commit $/x ) {
        $c->stash->{reap_me} = 1;
      }
    }
    
    $c->forward ( 'deserialize' );
  }
  else {
    $self->status_forbidden ( $c, message => 'Not even in your wildest dreams!' );
    
    $c->detach;
  }
}

sub deserialize : ActionClass('Deserialize') {}

sub end : Private {
  my ( $self, $c ) = @_;
  
  if ( $c->request->parameters->{hash} ) {
    if ( my $user_r = $c->model ( 'App::User' )->find ( $c->stash->{user_id} ) ) {
      for ( grep { $_->restful } $user_r->sessions ) {
        $c->log->info ( "Please reap " . $_->id . " session." );
      }

     #$self->idle ( $c );
      
      if ( $c->stash->{reap_me} ) {
        my $session_id = $c->sessionid;
        
        if ( my $session_r = $c->model ( 'App::Session' )->find ( $session_id ) ) {
          if ( $session_r->delete ) {
            $c->delete_session ( "reapable" );
            $c->log->info ( "Deleted $session_id session." );
          }
        }
      }
    }
  }
  
  $c->forward ( 'serialize' );
}

sub serialize : ActionClass('Serialize') {}

sub busy : Private {
  my ( $self, $c ) = @_;
  
  $c->model ( 'App' )->alter_session ({ c => $c, busy => 1, restful => 1 });
}

sub idle : Private {
  my ( $self, $c ) = @_;
  
  $c->model ( 'App' )->alter_session ({ c => $c, busy => 0, restful => 1 });
}

sub entity_or_message : Private {
  my ( $self, $c, $get_entity ) = @_;
  
  my ( $entity, $message );

  $self->busy ( $c );

  try {
    $entity  = $get_entity->( $c );
  }
  catch {
    $message = last_breath ( $_ );
  };

  $self->idle ( $c );
  
  return ( $entity, $message );
}


#
# Repositories.
#

=head2 repositories_GET

=over 4

=item * /rest/repositories

Returns a list of repositories.

=back

=cut

sub repositories_GET {
  my ( $self, $c ) = @_;
  
  $self->status_ok ( $c, entity => $c->model ( 'App' )->repositories_loh ( $c ) );
}

#
# Component.
#

=head2 component_POST

=over 4

=item * /rest/component?id=component_id_$component&field=$field&value=$value

Sets $field column to $value value on $component component (as per TableKit API).

=item * /rest/component?id=componentType_id_${component_type}&field=$field&value=$value

Sets $field column to $value value on $component_type component type (as per TableKit API).

=back

=cut

sub component_POST {
  my ( $self, $c ) = @_;
  
  my ( $entity, $message );
  
  if ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) {
    if ( $user_r->bofh ) {
      ( $entity, $message ) = $self->_boilerplate_POST ( $c, qr/^ ( component (?:Type)? ) _id_ ( .+? ) $/x );
    }
    else {
      $message = 'unauthorized user';
    }
  }
  else {
    $message = 'user not logged';
  }
  
  $self->_status ( $c, $entity, $message );
}

=head2 repository_POST

=over 4

=item * /rest/repository?id=repository_id_$repository&field=$field&value=$value

Sets $field column to $value value on $repository repository (as per TableKit API).

=item * /rest/repository?id=repositoryContent_id_${repository_content}&field=$field&value=$value

Sets $field column to $value value on $repository_content repository content (as per TableKit API).

=item * /rest/repository?id=repositoryType_id_${repository_type}&field=$field&value=$value

Sets $field column to $value value on $repository_type repository type (as per TableKit API).

=back

=cut

sub repository_POST {
  my ( $self, $c ) = @_;
  
  my ( $entity, $message );
  
  if ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) {
    if ( $user_r->bofh ) {
      ( $entity, $message ) = $self->_boilerplate_POST ( $c, qr/^ ( repository (?:Content|Type)? ) _id_ ( .+? ) $/x );
    }
    else {
      $message = 'unauthorized user';
    }
  }
  else {
    $message = 'user not logged';
  }
  
  $self->_status ( $c, $entity, $message );
}

=head2 components_GET

=over 4

=item * /rest/components ?type=$type

Returns a list of components hash.

=item * /rest/components/report?scope=specification|integration &product_ids=$product1|$product2 [ &type_ids=$type1|$type2 ]

Returns a list of components hash, targeted to a given product (and possibly type) lineup.

=back

=cut

sub components_GET {
  my ( $self, $c, $action ) = @_;
  
  my ( $entity, $message );
  
  my $parameter = $c->request->parameters;
  
  if ( $action ) {
    if ( $action eq 'report' ) {
      if ( $parameter->{scope} ) {
        $entity  = $c->model ( 'App' )->components_report ({
          c        => $c,
          scope    =>               $parameter->{scope},
          products => [ split '\|', $parameter->{product_ids} ],
          types    => [ split '\|', $parameter->{type_ids}    ],
        });
      }
      else {
        $message = "missing scope parameter";
      }
    }
    else {
      $message = "unknown \"$action\" action";
    }
  }
  else {
    my $where = $parameter->{type} ? { type_id => $parameter->{type} } : {};

    $entity = [ $c->model ( 'App::Component' )->search ( $where => { order_by => 'id', result_class => 'DBIx::Class::ResultClass::HashRefInflator' } ) ];
  }
  
  $self->_status ( $c, $entity, $message );
}

#
# Datasheet(s).
#

=head2 datasheets_GET

=over 4

=item * /rest/datasheets

Returns a list of datasheets.
Mostly used to cache "component" subversion repository for speed's sake.

=back

=cut

sub datasheets_GET {
  my ( $self, $c ) = @_;
  
  $self->status_ok ( $c, entity => $c->model ( 'App' )->datasheets_loh ( $c ) );
}

=head2 datasheet_GET

=over 4

=item * /rest/datasheet/$datasheet/mails

Returns sorted lists of mails of any_datasheet_event_mail/owner/writer/follower users.

=item * /rest/datasheet/$datasheet/products

Returns LoH of products.

=item * /rest/datasheet/$datasheet [ &tag=$tag | &branch=$branch ]

Queries datasheet, possibly with tag or branch.
Mostly used to cache "product" subversion repository for speed's sake.

=back

=cut

sub datasheet_GET {
  my ( $self, $c, $datasheet, $action ) = @_;
  
  $self->hit ( $c, datasheet => $datasheet );

  if ( $datasheet && $action ) {
    my ( $entity, $message );
    
    my $datasheet_r = $c->model ( 'App::Datasheet' )->find ( $datasheet );
    
    if ( !$datasheet_r && $datasheet =~ /^\d+$/ ) {
      my @datasheet_rs = $c->model ( 'App::Datasheet' )->search ({ id => { -like => '%_' .$datasheet  } });
      
      if ( scalar @datasheet_rs == 1 ) {
        ( $datasheet_r ) = @datasheet_rs;
        
        $datasheet = $datasheet_r->id;
      }
    }
    
    if ( $datasheet_r ) {
      if ( $action eq 'users' ) {
        my $loh = $datasheet_r->users_loh;
        my $id  = $c->model ( 'App' )->datasheet_mail_user_id_hash ({ datasheet_id => $datasheet });
        
        for ( @$loh ) {
          next unless my $mail = $id->{$_->{id}};

          $_->{mail} = $mail;
        }

        $entity  = $loh;
      }
      elsif ( $action eq 'mails' ) {
        $entity  = [ $c->model ( 'App' )->datasheet_mails ({ datasheet_id => $datasheet }) ];
      }
      elsif ( $action eq 'products' ) {
        try {
          $entity  = $c->model ( 'App' )->datasheet_products ({ datasheet_id => $datasheet });
        }
        catch {
          $message = last_breath ( $_ );
        };
      }
      elsif ( $action eq 'commits' ) {
        try {
          $entity  = $c->model ( 'App' )->datasheet_commits ({ datasheet_id => $datasheet });
        }
        catch {
          $message = last_breath ( $_ );
        };
      }
      else {
        $message = "unknown \"$action\" action";
      }
    }
    else {
      $message = "unknown \"$datasheet\" datasheet";
    }
    
    $self->_status ( $c, $entity, $message );
  }
  else {
    $self->get ( $c, 'datasheet' );
  }
}

=head2 datasheet_PUT

=over 4

=item * /rest/datasheet/$datasheet [ &tag=$tag | &branch=$branch ]

Declares datasheet, possibly with tag or branch.
Mostly used to cache "component" subversion repository for speed's sake.

=back

=cut

sub datasheet_PUT {
  my ( $self, $c, $datasheet ) = @_;
  
  $self->hit ( $c, datasheet => $datasheet );

  my ( $entity, $message );
  
  try {
    $entity  = $self->put ( $c, 'datasheet' );
  }
  catch {
    $message = last_breath ( $_ );
  };
  
  $self->_status ( $c, $entity, $message );
}

=head2 datasheet_POST

=over 4

=item * /rest/datasheet?id=user_id_${user_id}__datasheet_id_${datasheet_id}&field=$field&value=(yes|no)

Sets $field column to $value value on $datasheet datasheet (as per TableKit API).

=back

=cut

sub datasheet_POST {
  my ( $self, $c ) = @_;
  
  my ( $entity, $message );
  
  if ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) {
    if ( $user_r->bofh || $user_r->assert_user_role ( 'can_admin_datasheets' ) ) {
      my $parameter = $c->request->parameters;
      
      if ( my ( $user_id, $datasheet_id ) = ( $parameter->{id} =~ /^ user_id _ (.+?) __ datasheet_id _ (.+?) $/x ) ) {
        if ( $c->model ( 'App::Datasheet' )->find ( $datasheet_id ) ) {
          try {
            if ( my ( $authorization_id ) = ( $parameter->{field} =~ /^ authorization _ (\w+) $/x ) ) {
              my $model_m = $c->model ( 'App::UserToDatasheet' );
              
              if ( $parameter->{value} eq 'yes' ) {
                $model_m->update_or_create ({ user_id => $user_id, datasheet_id => $datasheet_id, authorization_id => $authorization_id });
                
                if ( $authorization_id eq 'owner' ) {
                  $_->delete for grep { defined } map { $model_m->find ( $user_id, $datasheet_id, $_ ) } qw(       writer follower );
                }
                elsif ( $authorization_id eq 'writer' ) {
                  $_->delete for grep { defined } map { $model_m->find ( $user_id, $datasheet_id, $_ ) } qw( owner        follower );
                }
                
                $entity = { value => $parameter->{value} };
              }
              elsif ( $parameter->{value} eq 'no' ) {
                if ( my $u2d_r = $model_m->find ( $user_id, $datasheet_id, $authorization_id ) ) {
                  $u2d_r->delete;
                }
                
                $entity = { value => $parameter->{value} };
              }
              else {
                $message = 'malformed request (ill value parameter)';
              }
            }
            else {
              $message = 'malformed request (ill field parameter)';
            }
          }
          catch {
            $message = 'database exception';
          };
        }
        else {
          $message = 'unknown datasheet';
        }
      }
      else {
        $message = 'malformed request (ill id parameter)';
      }
    }
    else {
      $message = 'unauthorized user';
    }
  }
  else {
    $message = 'user not logged';
  }
  
  $self->_status ( $c, $entity, $message );
}

#
# Commits.
#

=head2 commit_POST

=over 4

=item /rest/commit?repository_id=$repository_id [ dummy=1 ] [ test = 1 ] [ debug = 1 ]

Sends a commit mail to relevant recipients.

Requires a data structure like:

  {
    revision  => $revision,
    author    => $author,
    timestamp => $timestamp,
    message   => $message,
    changes   => [
      /^ [A-Z] \s+ $specification \/ .+ $/x,
    ],
  }

=back

=cut

sub commit_POST {
  my ( $self, $c ) = @_;
  
  my $parameter = $c->request->parameters;
  
  my ( $entity, $message );
  
  if ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) {
    if ( my $data = $c->request->data ) {
      if ( my $repository_id = $parameter->{repository_id} ) {
        if ( my $repository_r = $c->model ( 'App::Repository' )->find ( $repository_id ) ) {
          my $content_id = $repository_r->content_id;

          if ( $content_id eq 'specification' || $content_id eq 'datasheet' ) {
            if ( $data->{changes} ) {
              if ( my $user_r = $c->model ( 'App::User' )->search ({ -or => [ id => $data->{author}, subversion_id => $data->{author} ] })->first ) {
                my $first;
                   $first->{$_}++ for map { m/^ [A-Z] \s+ ([^\/]+) /x ; $1 } @{$data->{changes}};

                my @mails;

                my $error = sub {
                  push @mails, {
                    Subject => "$_[0] [ERROR]",
                    Data    => "$_[1]",
                  };
                };

                for my $whatever_id ( nsort uniq keys %$first ) {
                  my $subject = "[subversion] post-commit $content_id revision $data->{revision} ($whatever_id)";

                  if ( $user_r->can_alter ( $content_id, $whatever_id ) ) {
                    if ( my $specification_r = $c->model ( 'App::' . ucfirst $content_id )->find ( $whatever_id ) ) {
                      if ( my $to = join ', ', $c->model ( 'App' )->lurkers_mails ({ "${content_id}_id" => $whatever_id }) ) {
                        push @mails, {
                          To      => ( $parameter->{test} ? $c->config->{root_mail} : $to ),
                          Subject => $subject,
                          Data    =>
                              "Author     : " . $user_r->mail_address . "\n"
                            . "Timestamp  : $data->{timestamp}\n"
                            . "Repository : " . $repository_r->url . "\n"
                            . "Message    :\n" . join ( '', map {
                              "             $_\n" } split "\n", $data->{message} )
                            . "Changeset  :\n" . join ( '', map {
                              "             $_\n" } grep { m/^ [A-Z] \s+ $whatever_id \/ /x } @{$data->{changes}} ),
                        };
                        
                        try {
                          if ( $content_id eq 'datasheet' ) {
                            if ( my $datasheet_r = $c->model ( 'App::Datasheet' )->find ( $whatever_id ) ) {
                              if ( $datasheet_r->ahead ) {
                                if ( my $products = $c->model ( 'App' )->datasheet_products ({ datasheet_id => $whatever_id }) ) {
                                  if ( $products->{products} ) {
                                    my @products = grep { $_->{impacted} } @{$products->{products}};

                                    if ( @products ) {
                                      push @mails, {
                                        Subject => $subject . " [D2P]",
                                        Data    => Dump ( \@products ),
                                      };
                                    }
                                  }
                                }
                              }
                            }
                          }
                        };
                      }
                      else {
                        $error->( $subject => "No recipients!" );
                      }
                    }
                    else {
                      $error->( $subject => "Unknown $whatever_id $content_id!" );
                    }
                  }
                  else {
                    $error->( $subject => "Unauthorized " . $user_r->id . " user!" );
                  }
                }

                if ( $parameter->{dummy} ) {
                  $entity = \@mails;
                }
                else {
                  try {
                    $c->model ( 'Mail' )->send ({ c => $c, mails => \@mails, debug => $parameter->{debug} });
                    
                    $entity  = \@mails;
                  }
                  catch {
                    $message = $_;
                  };
                }
              }
              else {
                $message = "unkown \"$data->{author}\" user";
              }
            }
            else {
              $message = "no change";
            }
          }
          else {
            $message = "unkown \"$content_id\" content";
          }
        }
        else {
          $message = "unkown \"$repository_id\" data";
        }
      }
      else {
        $message = "missing \"repository_id\" parameter";
      }
    }
    else {
      $message = "no incoming data";
    }
  }
  else {
    $message = "user not logged";
  }
  
  $self->_status ( $c, $entity, $message );
}

#
# Specification(s).
#

=head2 specification_POST

=over 4

=item * /rest/specification?id=user_id_${user_id}__specification_id_${specification_id}&field=$field&value=(yes|no)

Sets $field column to $value value on $specification specification (as per TableKit API).

=item * /rest/specification?id=specification_id_$specification&field=$field&value=$value

Sets $field column to $value value on $specification specification (as per TableKit API).

Used by admin/specifications page.

=item * /rest/specification/$specification/milestone/$milestone?dummy=1

Declares a milestone, i.e., a $milestone-$timestamp tag on specification repository.

Currently supported milestones are ARM and TOOLS.

=back

=cut

sub specification_POST {
  my ( $self, $c, $specification, $action, $milestone ) = @_;
  
  my $parameter = $c->request->parameters;

  my ( $entity, $message );
  
  if ( $specification && ( $action eq 'milestone' ) && $milestone && $parameter->{_method} eq 'put' ) { # prototype quirk: PUT => POST + ( _method == 'put' || _method == 'delete' )
    if ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) { # FIXME factorize with specification_PUT
      if ( $user_r->bofh || $user_r->assert_user_role ( 'can_tools_harvest' ) ) { # milestones_all_specifications
        if ( $milestone ) {
          if ( $milestone =~ /^(ARM|TOOLS)$/ ) {
            try {
              $entity  = $c->model ( 'Specification' )->milestone ({ user_id => $user_r->id, specification_id => $specification, milestone => $milestone, dummy => $parameter->{dummy}, c => $c });
            }
            catch {
              $message = last_breath ( $_ );
            };
          }
          else {
            $message = "unsupported '$milestone' milestone parameter (use either ARM or TOOLS)";
          }
        }
        else {
          $message = "missing 'milestone' parameter";
        }
      }
      else {
        $message = 'unauthorized user';
      }
    }
    else {
      $message = 'user not logged';
    }
  }
  elsif ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) {
    if ( $user_r->bofh || $user_r->assert_user_role ( 'can_admin_specifications' ) ) {
      if ( my ( $user_id, $specification_id ) = ( $parameter->{id} =~ /^ user_id _ (.+?) __ specification_id _ (.+?) $/x ) ) {
        if ( $c->model ( 'App::Specification' )->find ( $specification_id ) ) {
          try {
            if ( my ( $authorization_id ) = ( $parameter->{field} =~ /^ authorization _ (\w+) $/x ) ) {
              my $model_m = $c->model ( 'App::UserToSpecification' );
              
              if ( $parameter->{value} eq 'yes' ) {
                $model_m->update_or_create ({ user_id => $user_id, specification_id => $specification_id, authorization_id => $authorization_id });
                
                if ( $authorization_id eq 'owner' ) {
                  $_->delete for grep { defined } map { $model_m->find ( $user_id, $specification_id, $_ ) } qw(       writer follower );
                }
                elsif ( $authorization_id eq 'writer' ) {
                  $_->delete for grep { defined } map { $model_m->find ( $user_id, $specification_id, $_ ) } qw( owner        follower );
                }
                
                $entity = { value => $parameter->{value} };
              }
              elsif ( $parameter->{value} eq 'no' ) {
                if ( my $u2d_r = $model_m->find ( $user_id, $specification_id, $authorization_id ) ) {
                  $u2d_r->delete;
                }
                
                $entity = { value => $parameter->{value} };
              }
              else {
                $message = 'malformed request (ill value parameter)';
              }
            }
            else {
              $message = 'malformed request (ill field parameter)';
            }
          }
          catch {
            $message = 'database exception';
          };
        }
        else {
          $message = 'unknown specification';
        }
      }
      else {
        ( $entity, $message ) = $self->_boilerplate_POST ( $c, qr/^ ( specification ) _id_ ( .+? ) $/x );
      }
    }
    else {
      $message = 'unauthorized user';
    }
  }
  else {
    $message = 'user not logged';
  }
  
  $self->_status ( $c, $entity, $message );
}

=head2 specifications_GET

=over 4

=item * /rest/specifications

Returns a list of specifications.
Mostly used to cache "product" subversion repository for speed's sake.

=back

=cut

sub specifications_GET {
  my ( $self, $c ) = @_;
  
  $self->status_ok ( $c, entity => $c->model ( 'App' )->specifications_loh ( $c ) );
}

=head2 specification_GET

=over 4

=item * /rest/specification/$specification/branches

Returns a naturally sorted list of branches.

=item * /rest/specification/$specification/tags

Returns a naturally sorted list of tags.

=item * /rest/specification/$specification/mails

Returns sorted lists of mails of any_specification_event_mail/owner/writer/follower users.

=item * /rest/specification/$specification/datasheets

Returns sorted lists of datasheets (hash like { id =>, version =>, description => }).

=item * /rest/specification/$specification [ &tag=$tag | &branch=$branch ]

Queries specification, possibly with tag or branch.
Mostly used to cache "product" subversion repository for speed's sake.

=item * /rest/specification/$specification/components

Returns specification component lineup.

=back

=cut

sub specification_GET {
  my ( $self, $c, $specification, $action ) = @_;
  
  $self->hit ( $c, specification => $specification );

  if ( $specification && $action ) {
    my ( $entity, $message );
    
    if ( my $specification_r = $c->model ( 'App::Specification' )->find ( $specification ) ) {
      if ( $action eq 'users' ) {
        my $loh = $specification_r->users_loh;
        my $id  = $c->model ( 'App' )->specification_mail_user_id_hash ({ specification_id => $specification });
        
        for ( @$loh ) {
          next unless my $mail = $id->{$_->{id}};

          $_->{mail} = $mail;
        }

        $entity  = $loh;
      }
      elsif ( $action eq 'mails' ) {
        $entity  = [ $c->model ( 'App' )->specification_mails      ({ specification_id => $specification }) ];
      }
      elsif ( $action eq 'datasheets' ) {
        $entity  = [ $c->model ( 'App' )->specification_datasheets ({ specification_id => $specification, c => $c }) ];
      }
      elsif ( $action eq 'branches' ) {
        $entity  = [ $c->model ( 'App' )->specification_branches   ({ specification_id => $specification }) ];
      }
      elsif ( $action eq 'tags' ) {
        $entity  = [ $c->model ( 'App' )->specification_tags       ({ specification_id => $specification }) ];
      }
      elsif ( $action eq 'commits' ) {
        try {
          $entity  = $c->model ( 'App' )->specification_commits    ({ specification_id => $specification });
        }
        catch {
          $message = last_breath ( $_ );
        };
      }
      elsif ( $action eq 'read' ) {
        try {
          $entity  = $c->model ( 'Specification' )->component_lineup ({ specification_id => $specification, c => $c });
        }
        catch {
          $message = last_breath ( $_ );
        };
      }
      else {
        $message = "unknown \"$action\" action";
      }
    }
    else {
      $message = "unknown \"$specification\" specification";
    }
    
    $self->_status ( $c, $entity, $message );
  }
  else {
    $self->get ( $c, 'specification' );
  }
}

=head2 specification_PUT

=over 4

=item * /rest/specification/$specification/pull?dummy=1

Parses specification MIF to update freeze/product_to_freeze tables for matching product(s) in DB.

Beware! Only works if specification is linked with a single product. 

=item * /rest/specification/$specification/integrate?dummy=1&changed=$changed

Triggers integration from specification/follow-up combo as per SOCK's sock_integrate_product tool.

=item * /rest/specification/$specification [ &tag=$tag | &branch=$branch ]

Declares specification, possibly with tag or branch.
Mostly used to cache "product" subversion repository for speed's sake.

=item * /rest/specification/$specification/milestone/$milestone?dummy=1

Declares a milestone, i.e., a $milestone-$timestamp tag on specification repository.

Currently supported milestones are ARM and TOOLS.

=back

=cut

sub specification_PUT {
  my ( $self, $c, $specification, $action, $milestone ) = @_;
  
  $self->hit ( $c, specification => $specification );

  my $parameter = $c->request->parameters;

  my ( $entity, $message );
  
  if ( $specification ) {
    if ( $action ) {
      if ( $action eq 'pull' ) {
        my $query = { specification_id => $specification };
        
        my $branch = $parameter->{branch};
        
        if ( defined $branch ) {
          $query->{specification_branch} = $branch;
        }
        
        my @product_rs = $c->model ( 'App::Product' )->search ( $query )->all;

        if ( scalar @product_rs == 1 ) {
          my $product_r = $product_rs[0];
          
          ( $entity, $message ) = $self->entity_or_message ( $c => sub {
            my $data = $c->model ( 'Specification' )->component_lineup ({ specification_id => $specification, specification_branch => $branch, c => $c });

            $data->{product_id}    = $product_r->id;
            $data->{product_scope} = 'specification';

            return $parameter->{debug} ? $data : $c->model ( 'App' )->feed_component_lineup ({ data => $data, dummy => $parameter->{dummy} });
          } );
        }
        elsif ( scalar @product_rs > 1 ) {
          $message = "specification matches more than one product (" . join ( ", ", nsort map { $_->id } @product_rs ) . ")";
        }
        else {
          $message = "specification does not match any product";
        }
      }
      elsif ( $action eq 'integrate' ) {
        if ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) {
          if ( $user_r->bofh || $user_r->assert_user_role ( 'can_integrate_specification' ) ) {
            ( $entity, $message ) = $self->entity_or_message ( $c => sub {
              return { log_lines => [
                split "\n", $c->model ( 'Specification' )->integrate ({
                  c                => $c,
                  specification_id => $specification,
                  dummy            => $parameter->{dummy},
                  test             => $parameter->{test},
                  changed          => $parameter->{changed},
                  branch           => $parameter->{branch},
                })
              ] };
            } );
          }
          else {
            $message = 'unauthorized user';
          }
        }
        else {
          $message = 'user not logged';
        }
      }
      elsif ( $action eq 'milestone' ) {
        if ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) { # FIXME factorize with specification_POST
          if ( $user_r->bofh || $user_r->assert_user_role ( 'milestones_all_specifications' ) ) { # can_tools_harvest
            if ( $milestone ) {
              if ( $milestone =~ /^(ARM|TOOLS)$/ ) {
                try {
                  $entity  = $c->model ( 'Specification' )->milestone ({ user_id => $user_r->id, specification_id => $specification, milestone => $milestone, dummy => $parameter->{dummy} });
                }
                catch {
                  $message = last_breath ( $_ );
                };
              }
              else {
                $message = "unsupported '$milestone' milestone parameter (use either ARM or TOOLS)";
              }
            }
            else {
              $message = "missing 'milestone' parameter";
            }
          }
          else {
            $message = 'unauthorized user';
          }
        }
        else {
          $message = 'user not logged';
        }
      }
      else {
        $message = "unkown '$action' action";
      }
    }
    else { # FIXME use a keyword instead of nothing
      try {
        $entity  = $self->put ( $c, 'specification' );
      }
      catch {
        $message = last_breath ( $_ );
      };
    }
  }
  
  $self->_status ( $c, $entity, $message );
}

#
# Processes.
#

sub processes_GET {
  my ( $self, $c ) = @_;
  
  $self->status_ok ( $c, entity => $c->model ( 'App' )->processes_loh );
}

#
# Stages.
#

sub stages_GET {
  my ( $self, $c ) = @_;
  
  $self->status_ok ( $c, entity => $c->model ( 'App' )->stages_loh );
}

#
# SCM types.
#

sub scm_types_GET {
  my ( $self, $c ) = @_;
  
  $self->status_ok ( $c, entity => $c->model ( 'App' )->scm_types_loh );
}

#
# Component types.
#

sub component_types_GET {
  my ( $self, $c ) = @_;
  
  $self->status_ok ( $c, entity => $c->model ( 'App' )->component_types_loh );
}

#
# Repository contents.
#

sub repository_contents_GET {
  my ( $self, $c ) = @_;
  
  $self->status_ok ( $c, entity => $c->model ( 'App' )->repository_contents_loh );
}

#
# Repository types.
#

sub repository_types_GET {
  my ( $self, $c ) = @_;
  
  $self->status_ok ( $c, entity => $c->model ( 'App' )->repository_types_loh );
}

#
# Bugzilla.
#

sub _bugzilla_model : Private {
  my ( $self, $c, $type ) = @_;
  
  if ( my $class = $c->model ( 'Bugzilla' )->class ( $type ) ) {
    return $c->model ( $class );
  }
  
  return;
}

#
# Products.
#

sub bugzilla_version_as_html_a_hash : Private {
  my ( $self, $option ) = @_;
  
  my ( $c, $name, $type, $version, $process ) = map { $option->{$_} } qw( c name type version process );
  
  if ( defined $version ) {
    if ( my $bugzilla_m = $self->_bugzilla_model ( $c, $type ) ) {
      my $bugzilla_name    = $bugzilla_m->component_name    ( $name, $process );
      my $bugzilla_version = $bugzilla_m->component_version ( $version );

      my $root             = $c->model ( 'App::ComponentType' )->find ( $type )->bug_tracker_url;

      if ( my $bz_product_r = $bugzilla_m->resultset ( 'Product' )->search ({ name => $bugzilla_name })->first ) {
        if ( $bugzilla_m->resultset ( 'Version' )->search ({ product_id => $bz_product_r->id, value => $bugzilla_version })->single ) {
          return { tag => 'a', href => $bz_product_r->bug_list_url ({ root => $root, version => $bugzilla_version }), title => "$bugzilla_name $bugzilla_version",        class => 'bugzilla_bug_list',            content => $version };
        }
        else {
          return { tag => 'a', href => $bz_product_r->bug_list_url ({ root => $root                               }), title => "$bugzilla_name (version does not exist)", class => 'bugzilla_bug_list_no_version', content => $version };
        }
      }
      else {
        return { tag => 'span', class => 'bugzilla_bug_list_no_product', title => "$bugzilla_name (does not exist)", content => $version },
      }
    }
  }
  
  return { tag => 'span', class => 'undefined', content => undef },
}

sub bugzilla_last_version_as_html_a_hash : Private {
  my ( $self, $option ) = @_;
  
  my ( $c, $name, $type, $process ) = map { $option->{$_} } qw( c name type process );

  if ( my $bugzilla_m = $self->_bugzilla_model ( $c, $type ) ) {
    my $bugzilla_name = $bugzilla_m->component_name ( $name, $process );

    if ( my $product_r = $bugzilla_m->resultset ( 'Product' )->search ({ name => $bugzilla_name })->first ) {
      my @versions = $product_r->sorted_versions;

      if ( @versions ) {
        my $bugzilla_version = $versions[-1];
        
        return {
          bug_tracker_versions     => \@versions,
          bug_tracker_last_version => {
            tag     => 'a',
            href    => $product_r->bug_list_url ({ root => $c->model ( 'App::ComponentType' )->find ( $type )->bug_tracker_url, version => $bugzilla_version }),
            title   => "$bugzilla_name $bugzilla_version",
            class   => 'bugzilla_bug_list',
            content => $bugzilla_version,
          },
        };
      }
    }
  }
  
  return;
}

sub product_to_component_lol : Private {
  my ( $self, $c, $scope ) = @_;
  
  my $neo;

  if ( my $lol = $c->model ( 'App' )->product_to_component_lol ( $scope ) ) {
    my ( undef, undef, undef, @products ) = @{ shift ( @$lol ) };
    push @$neo, [ 'Component', 'Description', 'Type', @products ];

    my @processes;
    for my $product ( @products ) {
      my $product_r = $c->model ( 'App::Product' )->find ( $product->{id} );
      
      push @processes, $product_r ? $product_r->process_id : undef;
    }
    
    for ( @$lol ) {
      my ( $name, $description, $type, @versions ) = @$_;
      
      my @lefties = ( $name, $description, $type );

      my $pushed;

      if ( $c->model ( 'App::Component' )->find ( $name ) ) {
        try {
          push @$neo, [ @lefties, map { $self->bugzilla_version_as_html_a_hash ({ c => $c, name => $name, type => $type, version => $versions[$_], process => $processes[$_] }) } ( 0 .. @products - 1 ) ];

          $pushed = 1;
        }
        catch {
          $c->log->error ( "could not search bugzilla database for '$name' component: $_" );
        };
      }

      push @$neo, [ @lefties, @versions ] unless $pushed;
    }
  }

  return $neo;
}

=head2 products_GET

=item * /rest/products/components

Returns products x components loH.

=item * /rest/products/radiator

Returns products radiator loH.

=item * /rest/products

Returns list of products.

=cut

sub products_GET {
  my ( $self, $c, $action ) = @_;
  
  my ( $entity, $message );
  
  if ( $action eq 'components' ) {
    if ( my $scope = $c->request->parameters->{scope} ) {
      $entity  = $self->product_to_component_lol ( $c, $scope );
    }
    else {
      $message = 'missing scope parameter';
    }
  }
  elsif ( $action eq 'radiator' ) {
    $entity = $c->model ( 'App' )->products_radiator_loh ( $c, 'rest' );
  }
  else {
    $entity = $c->model ( 'App' )->products_loh ( $c );
  }
  
  $self->_status ( $c, $entity, $message );
}

#
# Product.
#

=head2 product_GET

=item * /rest/product/$product/read?scope=specification OR /rest/product/$product/read/specification

Parses specification MIF.

=item * /rest/product/$product/read?scope=integration OR /rest/product/$product/read/integration PLUS [&url=$url] [&chip=$chip] [&technology=$technology]

Parses integration CONFIG.

=item * /rest/product/$product/read?scope=specification OR /rest/product/$product/read/specification

Scraps DFS intranet site.

=item * /rest/product/$product/bugs ( /integration OR ?scope=integration )? | ( /specification OR ?scope=specification )?

Returns product components bugs. Defaults to integration scope.

=item * /rest/product/$product/components

Returns product components.

=item * /rest/product/$product/datasheets

Returns product datasheets.

=item * /rest/product/$product/devices

Returns product devices.

=item * /rest/product/$product/history

Returns component history.

=item * /rest/product/$product/users

Returns product users and their authorizations.

=item * /rest/product/$product/stage

Returns product NPI stage.

=cut

sub product_GET {
  my ( $self, $c, $product, $action, $scope ) = @_;
  
  $self->hit ( $c, product => $product );
  
  my $parameter = $c->request->parameters;

  my ( $entity, $message );
  
  if ( $self->requested_components ( $c ) ) {
    if ( my $product_r = $c->model ( 'App::Product' )->find ( $product ) ) {
      $entity = $c->model ( 'App' )->product_components_loh ( $product );

      my $process = $product_r->process_id;

      for ( @$entity ) {
        next unless my $component_name = $_->{specification_name} || $_->{integration_name} || $_->{dfs_name}; # FIXME define component_name if consistent?
        
        if ( my $component_r = $c->model ( 'App::Component' )->find ( $component_name ) ) {
          next if $component_r->type->bug_tracker_type_id ne 'bugzilla';
        }
        
        $_->{specification_version} = $self->bugzilla_version_as_html_a_hash ({ c => $c, name => $_->{specification_name}, type => $_->{specification_type}, version => $_->{specification_version}, process => $process }) if $_->{specification_version};
        $_->{integration_version}   = $self->bugzilla_version_as_html_a_hash ({ c => $c, name => $_->{integration_name},   type => $_->{integration_type},   version => $_->{integration_version},   process => $process }) if $_->{integration_version};
        $_->{dfs_version}           = $self->bugzilla_version_as_html_a_hash ({ c => $c, name => $_->{dfs_name},           type => $_->{dfs_type},           version => $_->{dfs_version},           process => $process }) if $_->{dfs_version};
        
        if ( my $h = $self->bugzilla_last_version_as_html_a_hash ({ c => $c, name => $_->{integration_name}, type => $_->{integration_type}, process => $process }) ) { # FIXME use component_name instead?
          $_->{bug_tracker_versions}     = $h->{bug_tracker_versions};
          $_->{bug_tracker_last_version} = $h->{bug_tracker_last_version};
        }
      }
      
      if ( my $session_path = iSOCK::Controller::Root->session_path ( $c ) ) {
        my ( $author, $user_r );
        
        if ( $user_r = $c->model ( 'App' )->user_r ( $c ) ) {
        }
        elsif ( my $user_id = $parameter->{user_id} ) {
          $user_r = $c->model ( 'App::User' )->find ( $user_id );
        }
        
        $c->model ( 'App' )->product_components_loh_to_xls_file ({ product => $product, loh => $entity, file => "$session_path/$product-components.xls", author => $user_r ? $user_r->name : undef });
      }
    }
    else {
      $message = "unknown \"$product\" product";
    }
  }
  elsif ( $self->requested_bugs ( $c ) ) {
    try {
      $entity  = $c->model ( 'App' )->product_to_component_bug_loh ( $c, $product, ( $scope || $parameter->{scope} || 'integration' ), $parameter->{tracker} );
    }
    catch {
      $c->log->error ( "problem with product_to_component_bug_loh sub: $_" );
      
      $message = 'problem accessing bugzilla database';
    };
  }
  elsif ( $action eq 'read' ) {
    if ( my $scope = $scope || $parameter->{scope} ) {
      if ( my $product_r = $c->model ( 'App::Product' )->find ( $product ) ) {
        if ( $scope eq 'specification' ) {
          ( $entity, $message ) = $self->entity_or_message ( $c => sub {
            return $c->model ( 'Specification' )->component_lineup ({ product_id => $product, c => $c, specification_id => $product_r->specification_id });
          } );
        }
        elsif ( $scope eq 'integration' ) {
          ( $entity, $message ) = $self->entity_or_message ( $c => sub {
            return $c->model ( 'Integration'   )->component_lineup ({ product_id => $product, c => $c, chip => $parameter->{chip}, technology => $parameter->{technology} });
          } );
        }
        elsif ( $scope eq 'dfs' ) {
          ( $entity, $message ) = $self->entity_or_message ( $c => sub {
            return $c->model ( 'DFS'           )->component_lineup ({ product_id => $product, c => $c });
          } );
        }
        else {
          $message = 'unsupported scope parameter (specification, integration and dfs are supported)';
        }
      }
      else {
        $message = "unknown \"$product\" product";
      }
    }
    else {
      $message = 'missing scope parameter';
    }
  }
  elsif ( $action eq 'history' ) {
    $entity = $c->model ( 'App' )->product_history_loh ( $product );
  }
  elsif ( $action eq 'users' ) {
    my $user;
    
    for ( $c->model ( 'App::UserToProduct' )->search ( { product_id => $product }, { order_by => 'user_id' } )->all ) {
      push @{$user->{$_->user_id}->{authorizations}}, $_->authorization_id;
    }
    
    $entity = [ map { {
      id   => $_,
      name => $c->model ( 'App::User' )->find ( $_ )->name,
      map { ( $_ => 1 ) } sort @{$user->{$_}->{authorizations}},
    } } nsort keys %$user ];
  }
  elsif ( $action eq 'datasheets' ) {
    if ( my $product_r = $c->model ( 'App::Product' )->find ( $product ) ) {
      $entity  = [ $c->model ( 'App' )->product_datasheets ({ product_id => $product, issues => 1, c => $c }) ];
    }
    else {
      $message = "unknown \"$product\" product";
    }
  }
  elsif ( $action eq 'devices' ) {
    if ( my $product_r = $c->model ( 'App::Product' )->find ( $product ) ) {
      $entity  = [ $c->model ( 'App' )->product_devices ({ product_id => $product }) ];
    }
    else {
      $message = "unknown \"$product\" product";
    }
  }
  elsif ( $action eq 'stage' ) {
    if ( my $product_r = $c->model ( 'App::Product' )->find ( $product ) ) {
      $entity  = { id => $product, die_id => $product_r->die_id, stage => $product_r->stage_id };
    }
    else {
      $message = "unknown \"$product\" product";
    }
  }
  else {
    $message = "unsupported \"$action\" action";
  }
  
  $self->_status ( $c, $entity, $message );
  
#  if ( my ( $product, $role ) = ( $c->request->path =~ /^ rest \/ product \/ ([^\/]+) \/ role \/ (specification_(?:owner|writer|follower)s) $/x ) ) {
#    if ( my $product_r = $c->model ( 'App::Product' )->find ( $product ) ) {
#      $self->status_ok ( $c, entity => [ map { { name => $_->name, mail => '"' . $_->name . '" <' . $_->mail . '>' } } map { $c->model ( 'Self::Users' )->find ( $_ ) } split ' ', $product_r->get_column ( $role ) ] );
#    }
#    else {
#      $self->status_not_found ( $c, message => 'unknown product' );
#    }
#  }
}

# FIXME POD me!

sub pull_product : Private {
  my ( $self, $c, $product, $scope ) = @_;
  
  my $parameter = $c->request->parameters;
  
  my ( $entity, $message );
  
  $scope ||=  $parameter->{scope};
  $scope   =~ s/&.+$//;
  
  if ( $scope ) {
    if ( my $product_r = $c->model ( 'App::Product' )->find ( $product ) ) {
      if ( $scope eq 'specification' ) {
        ( $entity, $message ) = $self->entity_or_message ( $c => sub {
          my $data = $c->model ( 'Specification' )->component_lineup ({ product_id => $product, specification_id => $product_r->specification_id, c => $c });
          
          if ( $parameter->{debug} ) {
            return $data;
          }
          else {
            my $actions = $c->model ( 'App' )->feed_component_lineup ({ data => $data, dummy => $parameter->{dummy} });

            return { data => $data, actions => $actions };
          }
        } );
      }
      elsif ( $scope eq 'integration' ) {
        ( $entity, $message ) = $self->entity_or_message ( $c => sub {
          my $data = $c->model ( 'Integration' )->component_lineup ({ product_id => $product, chip => $parameter->{chip}, technology => $parameter->{technology}, dummy => $parameter->{dummy}, c => $c });
          
          if ( $parameter->{debug} ) {
            return $data;
          }
          else {
            my $actions = $c->model ( 'App' )->feed_component_lineup ({ data => $data, dummy => $parameter->{dummy} });

            return { data => $data, actions => $actions };
          }
        } );
      }
      elsif ( $scope eq 'dfs' ) {
        ( $entity, $message ) = $self->entity_or_message ( $c => sub {
          my $data = $c->model ( 'DFS' )->component_lineup ({ product_id => $product, dummy => $parameter->{dummy}, c => $c });
          
          if ( $parameter->{debug} ) {
            return $data;
          }
          else {
            my $actions = $c->model ( 'App' )->feed_component_lineup ({ data => $data, dummy => $parameter->{dummy} });

            return { data => $data, actions => $actions };
          }
        } );
      }
      else {
        $message = "unsupported \"$scope\" parameter (specification, integration and dfs are supported)";
      }
    }
    else {
      $message = "unknown \"$product\" product";
    }
  }
  else {
    $message = 'missing scope parameter';
  }
  
  return ( $entity, $message );
}

# FIXME POD me!

sub push_product : Private {
  my ( $self, $c, $product, $scope ) = @_;
  
  my $parameter = $c->request->parameters;
  
  my ( $entity, $message );
  
  $scope ||=  $parameter->{scope};
  $scope   =~ s/&.+$//;
  
  if ( my $data = $parameter->{data} ? decode_json ( $parameter->{data} ) : $c->request->data ) { # bot.js || cURL
    if ( $scope ) {
      if ( my $product_r = $c->model ( 'App::Product' )->find ( $product ) ) {
        if ( $scope =~ /^(specification|integration|dfs)$/ ) {
          $data->{product_id}    = $product unless $data->{product_id};
          $data->{product_scope} = $scope   unless $data->{product_scope};
          
          ( $entity, $message ) = $self->entity_or_message ( $c => sub {
            if ( $parameter->{debug} ) {
              return $data;
            }
            else {
              my $actions = $c->model ( 'App' )->feed_component_lineup ({ data => $data, dummy => $parameter->{dummy} });

              return { data => $data, actions => $actions };
            }
          } );
        }
        else {
          $message = "unsupported \"$scope\" parameter (specification, integration and dfs are supported)";
        }
      }
      else {
        $message = "unknown \"$product\" product";
      }
    }
    else {
      $message = 'missing scope parameter';
    }
  }
  else {
    $message = "empty data";
  }
  
  return ( $entity, $message );
}

=head2 product_PUT

=over 4

=item * /rest/product/$product/pull?scope=specification OR /rest/product/$product/pull/specification [&debug=1] [&dummy=1]

Parses specification MIF to update freeze/product_to_freeze tables.

=item * /rest/product/$product/pull?scope=integration OR /rest/product/$product/pull/integration [&url=$url] [&chip=$chip] [&technology=$technology] [&debug=1] [&dummy=1]

Parses integration CONFIG to update freeze/product_to_freeze tables.

=item * /rest/product/$product/pull?scope=dfs OR /rest/product/$product/pull/dfs [&dummy=1]

Scraps DFS site to update freeze/product_to_freeze tables.

=item * /rest/product/$product/components?dummy=1

Updates freeze/product_to_freeze tables with fed serialized data:

  {
    product_scope => /^(specification|integration)$/,
    product_id    => $product_id,
    components    => [
      { version => $version, name => $name, type => /^(analog|digital|ip|memory)$/ },
    ],
  }

=back

=cut

sub product_PUT {
  my ( $self, $c, $product, $action, $target ) = @_;
  
  $self->hit ( $c, product => $product );

  my $parameter = $c->request->parameters;

  my ( $entity, $message );
  
  if ( $action eq 'components' ) {
    if ( my $data = $c->request->data ) {
      if ( $data->{product_id} eq $product ) {
        ( $entity, $message ) = $self->entity_or_message ( $c => sub {
          return $c->model ( 'App' )->feed_component_lineup ({ data => $data, dummy => $parameter->{dummy} });
        } );
      }
      else {
        $message = "product name discrepancy between URL and data";
      }
    }
    else {
      $message = "empty data";
    }
  }
  elsif ( $action eq 'pull' ) {
    ( $entity, $message ) = $self->pull_product ( $c, $product, $target );
  }
  elsif ( $action eq 'push' ) {
    ( $entity, $message ) = $self->push_product ( $c, $product, $target );
  }
 
  $self->_status ( $c, $entity, $message );
}

=head2 product_POST

=over 4

=item * /rest/product?id=product_id_$product&field=$field&value=$value

Sets $field column to $value value on $product product (as per TableKit API).

=item * /rest/product/$product/pull

As per L</product_PUT> sub.
This is a quirk from Prototype framework (as per L<http://api.prototypejs.org/ajax/>).

=back

=cut

sub product_POST {
  my ( $self, $c, $product, $action, $target ) = @_;
  
  my $parameters = $c->request->parameters;

  my ( $entity, $message );
  
  if ( $product && $action ) { # && $parameters->{_method} eq 'put' ~ prototype quirk: PUT => POST + ( _method == 'put' || _method == 'delete' )
    if ( $action eq 'pull' ) {
      $self->hit ( $c, product => $product );

      ( $entity, $message ) = $self->pull_product ( $c, $product, $target );
    }
    elsif ( $action eq 'push' ) {
      $self->hit ( $c, product => $product );

      ( $entity, $message ) = $self->push_product ( $c, $product, $target );
    }
    else {
      $message = "unsupported \"$action\" action";
    }
  }
  else {
    if ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) {
      my ( $product ) = ( $parameters->{id} =~ /^ product_id_ (.+) $/x );

      if ( $user_r->bofh || $c->model ( 'App::UserToProduct' )->find ( $user_r->id, $product, 'owner' ) ) {
        if ( my $product_r = $c->model ( 'App::Product' )->find ( $product ) ) {
          my ( $field, $value ) = ( $parameters->{field}, $parameters->{value} );
          
          $product_r->set_column ( $field => $value );
          
          if ( $field eq 'integration_scm_url' && $value =~ /^svn:/ ) {
            $product_r->set_column ( integration_scm_type_id => 'subversion' );
          }

          try {
            $product_r->update;
            
            $entity  = { value => $value };
          }
          catch {
            $message = 'problem during update';
          };
        }
        else {
          $message = "unknown \"$product\" product";
        }
      }
      else {
        $message = 'unauthorized user';
      }
    }
    else {
      $message = 'user not logged';
    }
  }
  
  $self->_status ( $c, $entity, $message );
}

#
# Generic (specifications & datasheets).
#

sub put {
  my ( $self, $c, $what ) = @_;
  
  my $model_what   = 'App::' . ucfirst ( $what );
  my $model_tag    = 'App::' . ucfirst ( $what ) . 'Tag';
  my $model_branch = 'App::' . ucfirst ( $what ) . 'Branch';
  
  if ( my $repository_id = $c->request->parameters->{repository_id} ) {
    if ( $c->model ( 'App::Repository' )->find ( $repository_id ) ) {
      if ( my $name = _normalize_spaces ( $self->requested_name ( $c ) ) ) {
        if ( my $what_r = $c->model ( $model_what )->update_or_create ({ id => $name, repository_id => $repository_id }) ) {
          if ( my $tag = _normalize_spaces ( $self->requested_tag ( $c ) ) ) {
            if ( $c->model ( $model_tag )->update_or_create ({ name => $tag, owner => $name }) ) {
              if ( $tag eq $what_r->last_tag ) {
                $what_r->update ({ ahead => 0 });
              }

              return { success => 1 };
            }
            else {
              die "could not update/create $tag tag on $name $what";
            }
          }
          elsif ( my $branch = _normalize_spaces ( $self->requested_branch ( $c ) ) ) {
            if ( $c->model ( $model_branch )->update_or_create ({ name => $branch, owner => $name }) ) {
              return { success => 1 };
            }
            else {
              die "could not create $branch branch on $name $what";
            }
          }
          else {
            $what_r->update ({ ahead => 1 });
            
            return { success => 1 };
          }
        }
        else {
          die "could not update/create $name $what";
        }
      }
      else {
        die "missing $what name";
      }
    }
    else {
      die "unknown $repository_id repository";
    }
  }
  else {
    die "missing repository_id parameter";
  }
}

sub get {
  my ( $self, $c, $what ) = @_;
  
  my $model_what   = 'App::' . ucfirst ( $what );
  my $model_tag    = 'App::' . ucfirst ( $what ) . 'Tag';
  my $model_branch = 'App::' . ucfirst ( $what ) . 'Branch';
    
  my ( $entity, $message );
  
  if ( my $name = $self->requested_name ( $c ) ) {
    if ( my $model_r = $c->model ( $model_what )->find ( $name ) ) {
      $name = _normalize_spaces ( $name );
      
      if ( $self->requested_tags ( $c ) ) {
        $entity = [ sort map { $_->name } $c->model ( $model_tag    )->search ({ owner => $name }) ];
      }
      elsif ( $self->requested_branches ( $c ) ) {
        $entity = [ sort map { $_->name } $c->model ( $model_branch )->search ({ owner => $name }) ];
      }
      elsif ( my $tag = $self->requested_tag ( $c ) ) {
        $tag = _normalize_spaces ( $tag );
        
        if ( $c->model ( $model_tag )->search ({ name => $tag, owner => $name })->single ) {
          $entity  = { success => 1 };
        }
        else {
          $message = "$tag tag does not exist on $name $what";
        }
      }
      elsif ( my $branch = $self->requested_branch ( $c ) ) {
        $branch = _normalize_spaces ( $branch );
        
        if ( $c->model ( $model_branch )->search ({ name => $branch, owner => $name })->single ) {
          $entity  = { success => 1 };
        }
        else {
          $message = "$branch branch does not exist on $name $what";
        }
      }
      else {
        $entity = $c->model ( 'App' )->whatever_hoh ({ c => $c, whatever => $what, whatever_r => $model_r });
      }
    }
    else {
      $message = "$name $what does not exist";
    }
  }
  else {
    $message = "missing $what name";
  }  
  
  $self->_status ( $c, $entity, $message );
}

#
# User.
#

sub user_POST {
  my ( $self, $c, $user, $action, $target ) = @_;
  
  my ( $entity, $message );
  
  if ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) {
    my $parameters = $c->request->parameters;

    if ( $user && $action && $parameters->{_method} eq 'put' ) { # prototype quirk: PUT => POST + ( _method == 'put' )
      my $authorization_id = $parameters->{authorization} || 'follower';

      if ( $action eq 'product' ) {
        if ( $user_r->bofh || $user_r->assert_user_role ( 'can_admin_products' ) ) {
          if ( $c->model ( 'App::UserToProduct' )->create ({ user_id => $user, product_id => $target, authorization_id => $authorization_id }) ) {
            $entity  = { OK => 1 };
          }
          else {
            $message = 'problem during user-to-product update/create';
          }
        }
        else {
          $message = 'unauthorized user';
        }
      }
      elsif ( $action eq 'specification' ) {
        if ( $user_r->bofh || $user_r->assert_user_role ( 'can_admin_specifications' ) ) {
          if ( $c->model ( 'App::UserToSpecification' )->update_or_create ({ user_id => $user, specification_id => $target, authorization_id => $authorization_id }) ) {
            $entity  = { OK => 1 };
          }
          else {
            $message = 'problem during user-to-specification update/create';
          }
        }
        else {
          $message = 'unauthorized user';
        }
      }
      elsif ( $action eq 'datasheet' ) {
        if ( $user_r->bofh || $user_r->assert_user_role ( 'can_admin_datasheets' ) ) {
          if ( $c->model ( 'App::UserToDatasheet' )->update_or_create ({ user_id => $user, datasheet_id => $target, authorization_id => $authorization_id }) ) {
            $entity  = { OK => 1 };
          }
          else {
            $message = 'problem during user-to-datasheet update/create';
          }
        }
        else {
          $message = 'unauthorized user';
        }
      }
      else {
        $message = "unsupported \"$action\" action";
      }
    }
    else {
      if ( $user_r->bofh ) {
        if ( my ( $id, $what, $what_id ) = ( $parameters->{id} =~ /^ user_id_ ([\w\.-]+?) (?: __ ( product_id | specification_id | datasheet_id ) _ ([\w-]+) )? $/x ) ) {
          if ( my $value = $c->model ( 'User' )->update ({ c => $c, id => $id, $what => $what_id, field => $parameters->{field}, value => $parameters->{value} }) ) {
            $entity  = { value => $value };
          }
          else {
            $message = 'problem during update';
          }
        }
        else {
          $message = "unsupported id=\"$parameters->{id}\" parameter";
        }
      }
      else {
        $message = 'unauthorized user';
      }
    }
  }
  else {
    $message = 'user not logged';
  }
  
  $self->_status ( $c, $entity, $message );
}

#=head2 user_GET
#
#=over 4
#
#=item * /rest/dstp/user/$user/product/$product/role/$role
#
#Returns { true => 1 } if $user has $role on $product, else { false => 1 }.
#
#=item * /rest/dstp/user/$user/specification/$specification/role/$role
#
#Returns { true => 1 } if $user has $role on $specification, else { false => 1 }.
#
#=back
#
#=cut
#
#sub user_GET {
#  my ( $self, $c, $user, $action2, $id2, $action3, $id3 ) = @_;
#  
#  my ( $entity, $message );
#  
#  if ( $user ) {
#    if ( my $user_r = $c->model ( 'App::User' )->find ( $user ) ) {
#      if ( $action2 ) {
#        if ( $action2 eq 'specification' ) {
#          if ( my $specification_r = $c->model ( 'App::Specification' )->find ( $id2 ) ) {
#            if ( $action3 eq 'role' ) {
#              $entity  = $c->model ( 'App::UserToSpecification' )->find ( $user, $id2, $id3 ) ? { true => 1 } : { false => 1 };
#            }
#            else {
#              $message = "unsupported \"$action3\" action";
#            }
#          }
#          else {
#            $message = "unknown \"$id2\" specification";
#          }
#        }
#        elsif ( $action2 eq 'product' ) {
#          if ( my $product_r = $c->model ( 'App::Product' )->find ( $id2 ) ) {
#            if ( $action3 eq 'role' ) {
#              $entity  = $c->model ( 'App::UserToProduct' )->find ( $user, $id2, $id3 ) ? { true => 1 } : { false => 1 };
#            }
#            else {
#              $message = "unsupported \"$action3\" action";
#            }
#          }
#          else {
#            $message = "unknown \"$id2\" product";
#          }
#        }
#        else {
#          $message = "unsupported \"$action2\" action";
#        }
#      }
#      else {
#        $entity = $user_r->authorizations_h;
#      }
#    }
#    else {
#      $message = "unknown \"$user\" user";
#    }
#  }
#  
#  $self->_status ( $c, $entity, $message );
#}

=head2 users_GET

=over 4

=item * /rest/dstp/users

Returns a LoH like:

  {
    id   => $id,
    name => $name,
    products => [
      {
        id       => $product_id,
        name     => $product_name,
        owner    => $bool,
        writer   => $bool,
        follower => $bool,
      }
    ],
    specifications => [
      {
        id       => $product_id,
        owner    => $bool,
        writer   => $bool,
        follower => $bool,
      }
    ],
  }

=back

=cut

sub users_GET {
  my ( $self, $c ) = @_;
    
  $self->status_ok ( $c, entity => [ map { $_->authorizations_h } $c->model ( 'App::User' )->search ( {}, { order_by => 'name' } ) ] );
}

=head2 user_GET

=over 4

=item * /rest/user/$user/authorization/(owner|writer|follower|tinkerer)/product/$product [&creation=1]

=item * /rest/user/$user/authorization/(owner|writer|follower|tinkerer)/specification/$specification [&creation=1]

=item * /rest/user/$user/authorization/(owner|writer|follower|tinkerer)/datasheet/$datasheet [&creation=1]

Special tinkerer authorization is a shortcut for either owner or writer.

=item * /rest/user/$user

=back

=cut

sub user_GET {
  my ( $self, $c ) = @_;
  
  if ( my ( $user ) = $c->request->path =~ /^ rest \/ user \/ ([^\/]+) /x ) {
    my ( $authorization, $product, $whatever, $whatever_id );
    
    my $user_r = $c->model ( 'App::User' )->find ( $user );
    
    unless ( $user_r ) {
      if ( $user_r = $c->model ( 'App::User' )->search ({ subversion_id => $user })->first ) {
        $user = $user_r->id;
      }
    }
    
    if ( $user_r ) {
      if ( ( $authorization, $whatever, $whatever_id ) = ( $c->request->path =~ /^ rest \/ user \/ [^\/]+ \/ authorization \/ (owner|writer|follower|tinkerer) \/ (datasheet|specification|product) \/ ([^\/]+) $/x ) ) {
        my $Whatever  = ucfirst $whatever;
        my $whateverz = $whatever . 's';
        
        if ( $c->request->parameters->{creation} ) {
          if ( $user_r->bofh ) {
            $self->status_ok ( $c, entity => { user => $user, authorization => "can_bofh",               $whatever => $whatever_id } );
          }
          elsif ( $user_r->assert_user_role ( "can_admin_${whatever}s" ) ) {
            $self->status_ok ( $c, entity => { user => $user, authorization => "can_admin_${whatever}s", $whatever => $whatever_id } );
          }
          else {
            $self->status_bad_request ( $c, message => 'not authorized' );
          }
        }
        else {
          if ( my $whatever_r = $c->model ( "App::$Whatever" )->find ( $whatever_id ) ) {
            if ( $whatever_r->busy ) {
              $self->status_bad_request ( $c, message => 'busy' );
            }
            
            elsif ( $c->model ( "App::UserToRole" )->find ( $user, "owns_all_$whateverz"   ) && !$whatever_r->secure ) {
              $self->status_ok ( $c, entity  => { user => $user, authorization => "owns_all",     $whatever => $whatever_id } );
            }
            elsif ( $c->model ( "App::UserToRole" )->find ( $user, "writes_all_$whateverz" ) && !$whatever_r->secure ) {
              $self->status_ok ( $c, entity  => { user => $user, authorization => "writes_all",   $whatever => $whatever_id } );
            }
            
            elsif ( $authorization eq 'tinkerer' && $c->model ( "App::UserTo$Whatever" )->find ( $user, $whatever_id, 'owner'  ) ) {
              $self->status_ok ( $c, entity  => { user => $user, authorization => "owner",        $whatever => $whatever_id } );
            }
            elsif ( $authorization eq 'tinkerer' && $c->model ( "App::UserTo$Whatever" )->find ( $user, $whatever_id, 'writer' ) ) {
              $self->status_ok ( $c, entity  => { user => $user, authorization => "writer",       $whatever => $whatever_id } );
            }
            
            elsif ( $c->model ( "App::UserTo$Whatever" )->find ( $user, $whatever_id, $authorization ) ) {
              $self->status_ok ( $c, entity  => { user => $user, authorization => $authorization, $whatever => $whatever_id } );
            }
            
            else {
              $self->status_bad_request ( $c, message => 'not authorized' );
            }
          }
          else {
            $self->status_bad_request ( $c, message => "unknown \"$whatever_id\" $whatever" );
          }
        }
      }
      elsif ( $c->request->path =~ /^ rest \/ user \/ [^\/]+ $/x ) {
        $self->status_ok ( $c, entity  => $user_r->authorizations_h );
      }
      else {
        $self->status_bad_request ( $c, message => 'does not compute' );
      }
    }
    else {
      $self->status_bad_request ( $c, message => "unknown \"$user\" user" );
    }
  }
  else {
    $self->status_bad_request ( $c, message => 'does not compute' );
  }
}

#
# Hits.
#

=head2 dstp_GET

=over 4

=item * /rest/hits

Returns a list of { name => $id, type => $type, count => $count } hashes.

Mostly a L<ProtoCloud|https://github.com/tfluehr/ProtoCloud> experiment.

=back

=cut

sub hits_GET {
  my ( $self, $c ) = @_;
  
  my ( $entity, $message );
  
  try {
   #$entity  = [ sort { ncmp ( $a->{id}, $b->{id} ) } $c->model ( 'App::Hit' )->search ( undef, { result_class => 'DBIx::Class::ResultClass::HashRefInflator' } ) ];
    $entity  = [ sort { ncmp ( $a->{name}, $b->{name} ) } map { { name => $_->id, type => $_->type_id, count => $_->count } } $c->model ( 'App::Hit' )->search ];
  }
  catch {
    $message = last_breath ( $_ );
  };
  
  $self->_status ( $c, $entity, $message );
}

#
# Search.
#

sub search_GET {
  my ( $self, $c ) = @_;
  
  my ( $entity, $message );
  
  try {
    $entity  = $c->model ( 'App' )->found_products_loh ({ c => $c, parameter => $c->request->parameters });
  }
  catch {
    $message = last_breath ( $_ );
  };
  
  $self->_status ( $c, $entity, $message );
}

#
# Job.
#

=head2 job_GET

=over 4

=item * /rest/job/$job

Returns property hash.

=item * /rest/job/$job/column/$column

Returns { column => $value } hash.

=back

=cut

sub job_GET {
  my ( $self, $c, $id, $action, $column ) = @_;
  
  my ( $entity, $message );
  
  if ( my $job_r = $c->model ( 'App::Job' )->find ( $id ) ) {
    if ( $action && ( $action eq 'column' ) && $column ) {
      try {
        $entity  = { column => $job_r->get_column ( $column ) };
      }
      catch {
        $message = last_breath ( $_ );
      };
    }
    else {
      $entity = { $job_r->get_columns };
    }
  }
  else {
    $message = "unknown \"$id\" job";
  }
  
  $self->_status ( $c, $entity, $message );
}

=head2 last_job_GET

=over 4

=item * /rest/last_job

Returns last job property hash.

=back

=cut

sub last_job_GET {
  my ( $self, $c ) = @_;
  
  my ( $entity, $message );
  
  try {
    if ( my $job_r = $c->model ( 'App::Job' )->search ( {}, { order_by => { -desc => 'id' } } )->first ) {
      $entity  = { $job_r->get_columns };
    }
    else {
      $message = "no job";
    }
  }
  catch {
    $message = last_breath ( $_ );
  };
  
  $self->_status ( $c, $entity, $message );
}

#
# Harvest.
#

=head2 harvest_GET

=over 4

=item * /rest/harvest/$harvest

Returns property hash.

=item * /rest/harvest/$harvest/column/$column

Returns { column => $value } hash.

=item * /rest/harvest/$harvest/status

Returns { value => $status } hash.

=back

=cut

sub harvest_GET {
  my ( $self, $c, $id, $action, $column ) = @_;
  
  my ( $entity, $message );
  
  if ( my $harvest_r = $c->model ( 'App::Harvest' )->find ( $id ) ) {
    if ( $action && ( $action eq 'column' ) && $column ) {
      try {
        $entity  = { column => $harvest_r->get_column ( $column ) };
      }
      catch {
        $message = last_breath ( $_ );
      };
    }
    elsif ( $action eq 'status' ) {
      $entity = { value => $harvest_r->status_id };
    }
    else {
      $entity = { $harvest_r->get_columns };
    }
  }
  else {
    $message = "unknown \"$id\" harvest";
  }
  
  $self->_status ( $c, $entity, $message );
}

=head2 last_harvest_GET

=over 4

=item * /rest/last_harvest

Returns last harvest property hash.

=back

=cut

sub last_harvest_GET {
  my ( $self, $c ) = @_;
  
  my ( $entity, $message );
  
  try {
    if ( my $harvest_r = $c->model ( 'App::Harvest' )->search ( {}, { order_by => { -desc => 'id' } } )->first ) {
      $entity  = { $harvest_r->get_columns };
    }
    else {
      $message = "no harvest";
    }
  }
  catch {
    $message = last_breath ( $_ );
  };
  
  $self->_status ( $c, $entity, $message );
}

=head2 harvest_PUT

=over 4

=item * /rest/harvest/$harvest/build/$build OR /rest/harvest/$harvest?build=$build

Defines remote harvest build ID and toggles state to "harvested".

=item * /rest/harvest/$harvest OR /rest/harvest/$harvest

Toggles state to "completed".

=back

=cut

sub harvest_PUT {
  my ( $self, $c, $id, $column, $value ) = @_;
  
  my ( $entity, $message );
  
  if ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) {
    if ( $user_r->bofh || $user_r->assert_user_role ( 'can_alter_harvests' ) ) {
      my $parameter = $c->request->parameters;
      
      if ( my $harvest_r = $c->model ( 'App::Harvest' )->find ( $id ) ) {
        if ( my $build = ( $column && $value && $column eq 'build' ) ? $value : $parameter->{build} ) {
          if ( my $id = $harvest_r->build_id ) {
            $message = "already related to \"$id\" build";
          }
          else {
            try {
              $harvest_r->update ({ build_id => $build, status_id => 'harvested' });

              $entity  = { build => $build, status => 'harvested' };
            }
            catch {
              $message = last_breath ( $_ );
            };
          }
        }
        else {
          if ( $harvest_r->build_id ) {
            $harvest_r->update ({ status_id => 'completed' });

            $entity  = { status => 'completed' };
          }
          else {
            $message = 'no "build" state';
          }
        }
      }
      else {
        $message = "unknown \"$id\" harvest";
      };
    }
    else {
      $message = 'unauthorized user';
    }
  }
  else {
    $message = 'user not logged';
  }
  
  $self->_status ( $c, $entity, $message );
}

#
# DSTP.
#

=head2 dstp_GET

=over 4

=item * /rest/dstp/deliverables [?name=$name] [&version=$version] [&type=$type] [&status=$status]

=item * /rest/dstp/issues?deliverable=$id

=item * /rest/dstp/deliverable/$deliverable/versions

=item * /rest/dstp/deliverable/$deliverable/version/$version/issues

=back

=cut

sub dstp_GET {
  my ( $self, $c, $what, $name, $sub_what, $version ) = @_;

  my $parameter = $c->request->parameters;
  my $model     = $c->model ( 'DSTP' );

  if ( my $datasheet_r = $c->model ( 'App::Datasheet' )->search ({ id => { like => '%_' . $name } })->first ) {
    $self->hit ( $c, datasheet => $datasheet_r->id );
  }

  my ( $entity, $message );

  if ( $c->request->path =~ /^ rest \/ dstp \/ (?: deliverable | issue ) s? $/x ) {
    $entity = $model->query_result_loh ({ what => $what, map { $_ => $parameter->{$_} } qw( name version type status  deliverable ) });
  }
  elsif ( $c->request->path =~ /^ rest \/ dstp \/ deliverable \/ ([^\/]+) \/ versions $/x ) {
    $entity = $model->versions ( $name );
  }
  elsif ( $c->request->path =~ /^ rest \/ dstp \/ deliverable \/ ([^\/]+) \/ version \/ ([^\/]+) \/ issues $/x ) {
    $entity = $model->issues ( $name, $version );
  }
  
  $self->_status ( $c, $entity, $message, 'DSTP request failed' );
}

#
# Mail.
#

=head2 mail_POST

=over 4

=item * /rest/mail [debug=1]

Sends a mail via POST data (JSON-like):

  {
    "From"    : "\"Monsieur Chaussette\" <monsieur.chaussette@atmel.com>",
    "To"      : "\"Xavier Caron\" <xavier.caron@atmel.com>",
    "Subject" : "Bla",
    "Type"    : "TEXT",
    "Data"    : "Blabla"
  }

=back

=cut

sub mail_POST {
  my ( $self, $c ) = @_;
  
  my ( $entity, $message );

  if ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) {
    if ( $user_r->bofh ) {
      if ( my $data = $c->request->data ) {
        my $parameter = $c->request->parameters;
        
        try {
          $entity  = $c->model ( 'Mail' )->send ({ c => $c, mail => $data, debug => $parameter->{debug}, dummy => $parameter->{dummy} });
        }
        catch {
          $message = $_;
        };
      }
      else {
        $message = 'empty data';
      }
    }
    else {
      $message = 'unauthorized user';
    }
  }
  else {
    $message = 'user not logged';
  }
  
  $self->_status ( $c, $entity, $message );
}

#
# Devices.
#

sub devices_PUT {
  my ( $self, $c ) = @_;
  
  my ( $entity, $message );
  
  if ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) {
    if ( $user_r->bofh || $user_r->assert_user_role ( 'can_pull_devices' ) ) {
      my $parameter = $c->request->parameters;
      
      try {
        $entity  = $c->model ( 'App' )->pull_devices ({ c => $c, dummy => $parameter->{dummy} });
      }
      catch {
        $message = $_;
      };
    }
    else {
      $message = 'unauthorized user';
    }
  }
  else {
    $message = 'user not logged';
  }
  
  $self->_status ( $c, $entity, $message );
}

#
# Lines.
#

sub lines_GET {
  my ( $self, $c, $action ) = @_;
  
  my ( $entity, $message );
  
  if ( my $user_r = $c->model ( 'App' )->user_r ( $c ) ) {
    if ( $action eq 'harvestable' ) {
      my $parameter = $c->request->parameters;

      try {
        $entity  = $c->model ( 'App' )->harvestable_lines ({ dr_gte => $parameter->{dr_gte} });
      }
      catch {
        $message = $_;
      };
    }
    else {
      $message = "unsupported \"$action\" action";
    }
  }
  else {
    $message = 'user not logged';
  }
  
  $self->_status ( $c, $entity, $message );
}

#
# Helpers.
#

sub hit {
  my ( $self, $c, $type, $name ) = @_;
  
  $c->model ( 'App' )->hit ( $c, $type, $name );
  
  return $self;
}

sub _status : Private {
  my ( $self, $c, $entity, $message, $fallback ) = @_;
  
  $self->idle ( $c );
  
  if ( $entity ) {
    $self->status_ok          ( $c, entity  => $entity                                    );
  }
  else {
    $self->status_bad_request ( $c, message => $message || $fallback || 'guru meditation' );
  }
}

sub requested_name : Private {
  my ( $self, $c ) = @_;

  my $request = $c->request;
  
  $request->arguments->[0] || $request->parameters->{name};
}

sub requested_tags : Private {
  my ( $self, $c ) = @_;

  my $request = $c->request;
  
  ( $request->arguments->[1] && $request->arguments->[1] eq 'tags'     ) || $request->parameters->{tags};
}

sub requested_branches : Private {
  my ( $self, $c ) = @_;

  my $request = $c->request;
  
  ( $request->arguments->[1] && $request->arguments->[1] eq 'branches' ) || $request->parameters->{branches};
}

sub requested_tag : Private {
  my ( $self, $c ) = @_;

  my $request = $c->request;
  
  ( $request->arguments->[1] && $request->arguments->[1] eq 'tag'    ) ? $request->arguments->[2] : $request->parameters->{tag};
}

sub requested_branch : Private {
  my ( $self, $c ) = @_;

  my $request = $c->request;
  
  ( $request->arguments->[1] && $request->arguments->[1] eq 'branch' ) ? $request->arguments->[2] : $request->parameters->{branch};
}

sub requested_components : Private {
  my ( $self, $c ) = @_;

  my $request = $c->request;
  
  ( $request->arguments->[1] && $request->arguments->[1] eq 'components' ) || $request->parameters->{components};
}

sub requested_bugs : Private {
  my ( $self, $c ) = @_;

  my $request = $c->request;
  
  ( $request->arguments->[1] && $request->arguments->[1] eq 'bugs' ) || $request->parameters->{bugs};
}

sub _normalize_spaces : Private {
  my ( $string ) = @_;
  
  return unless $string;
  
  $string =~ s/^\s+//;
  $string =~ s/\s+$//;
  $string =~ s/\s+/ /g;
  
  $string;
}

#
# True.
#

1;
